package bancosys.tec.persist.authorization;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import jmine.tec.utils.Tuple;
import jmine.tec.utils.reflection.AnnotationUtils;

import org.hibernate.annotations.Filter;
import org.hibernate.annotations.Filters;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.hibernate.impl.annotations.AnnotationPartitionSessionFactory;

/**
 * Classe responsável pelo controle da Autorização. Essa classe é utilizada pelo BusinessPersistImpl. Ela deve ser configurada com um XML da
 * seguinte forma: <BOAuthorization><active>true </active> <objects><object> <class>bankware.frk.persist.test.bo.Test1BO </class>
 * <validatorClass>bankware.frk.persist.test.validator.Validator1BO </validatorClass> <displayName>Test 1 </displayName> </object> <object>
 * <class>bankware.frk.persist.test.bo.Test2BO </class> </object> </objects> </BOAuthorization>
 * 
 * @author Cesar Olmos
 */
public final class AuthorizationManager {

    /**
     * Armazena o status do authorization manager na thread atual.
     */
    private static final ThreadLocal<AuthorizationStatus> THREAD_LOCAL_STATUS = new ThreadLocal<AuthorizationStatus>();

    private AuthorizationStatus authorizationStatus;

    private boolean authBySameUser;

    private Set<Class<? extends Persistable>> exclusions = new HashSet<Class<? extends Persistable>>();

    /**
     * Construtor.
     * 
     * @param authorizationStatus informa o status da autorizacao padrao (utilizado para todas as threads, pode ser sobreescrito para uma
     * thread especifica).
     * @param authBySameUser <code>true</code> se é possível autorização pelo mesmo usuário, <code>false</code> caso contrário.
     */
    public AuthorizationManager(AuthorizationStatus authorizationStatus, boolean authBySameUser) {
        super();
        this.authorizationStatus = authorizationStatus;
        this.authBySameUser = authBySameUser;
    }

    /**
     * Define as exclusões para autorização.
     * 
     * @param exclusions as exclusões.
     */
    public void setExclusions(Collection<Class<? extends Persistable>> exclusions) {
        this.exclusions = new HashSet<Class<? extends Persistable>>(exclusions);
    }

    /**
     * Testa se um BO é autorizável ou não.
     * 
     * @param bo o bo a ser testado
     * @return <code>true</code> se o BO for autorizável, <code>false</code> caso contrário
     */
    public boolean isAuthorizable(Persistable bo) {
        return this.isAuthorizable(bo.getClass());
    }

    /**
     * Testa se um BO é autorizável ou não.
     * 
     * @param clazz classo do BO.
     * @return <code>true</code> se o BO for autorizável, <code>false</code> caso contrário
     */
    public boolean isAuthorizable(Class<? extends Persistable> clazz) {
        return Authorizable.class.isAssignableFrom(clazz) && !this.isExcluded(clazz) && hasAuthFilter(clazz);
    }

    /**
     * Returs true if the clazz (or one of its superclasses) has the {@link Filter} or {@link Filters} annotation, defining a filter named
     * {@link AnnotationPartitionSessionFactory#FILTER_NAME}
     * 
     * @param clazz the class
     * @return boolean
     */
    private boolean hasAuthFilter(Class<? extends Persistable> clazz) {
        Tuple<Class<?>, Filters> tupleFilters = AnnotationUtils.findClassAndAnnotationWithAnnotation(clazz, Filters.class);
        if (tupleFilters != null) {
            Filters filters = tupleFilters.getTail();
            org.hibernate.annotations.Filter[] filterAnnotations = filters.value();
            for (Filter filter : filterAnnotations) {
                if (AnnotationPartitionSessionFactory.FILTER_NAME.equals(filter.name())) {
                    return true;
                }
            }
        }
        Tuple<Class<?>, Filter> tupleFilter = AnnotationUtils.findClassAndAnnotationWithAnnotation(clazz, Filter.class);
        if (tupleFilter != null) {
            Filter filter = tupleFilter.tail();
            if (AnnotationPartitionSessionFactory.FILTER_NAME.equals(filter.name())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Se a autorização está habilitada nesse momento.
     * 
     * @return <code>true</code> se a autorização estiver ativada, <code>false</code> caso contrário.
     */
    public boolean isAuthorizationEnabled() {
        if (AuthorizationStatus.DISABLED.equals(this.authorizationStatus)) {
            return false;
        } else {
            return AuthorizationStatus.ENABLED.equals(this.getThreadAuthorizationStatus());
        }
    }

    /**
     * Se é autorizavel pelo mesmo usuário.
     * 
     * @return <code>true</code> se é possível autorização pelo mesmo usuário, <code>false</code> caso contrário.
     */
    public boolean isAuthorizableBySameUser() {
        return this.authBySameUser;
    }

    /**
     * Define o status da autorizacao para a thread corrente.
     * 
     * @param authorizationStatusContext o status de autorização para a thread corrente.
     */
    public void setThreadAuthorizationStatus(AuthorizationStatus authorizationStatusContext) {
        THREAD_LOCAL_STATUS.set(authorizationStatusContext);
    }

    /**
     * Retorna o status da autorizacao para a thread corrente.
     * 
     * @return o status da autorizacao para a thread corrente
     */
    public AuthorizationStatus getThreadAuthorizationStatus() {
        AuthorizationStatus threadAuthorizationStatus = THREAD_LOCAL_STATUS.get();
        return (threadAuthorizationStatus != null) ? threadAuthorizationStatus : this.authorizationStatus;
    }

    /**
     * @return the {@link AuthorizationStatus} associated with the current thread
     */
    public static AuthorizationStatus getCurrentThreadAuthorizationStatus() {
        return THREAD_LOCAL_STATUS.get();
    }

    /**
     * @param status the {@link AuthorizationStatus}
     */
    public static void setCurrentThreadAuthorizationStatus(AuthorizationStatus status) {
        THREAD_LOCAL_STATUS.set(status);
    }

    /**
     * Se uma dada classe está excluida da autorização.
     * 
     * @param clazz a classe a ser verificada.
     * @return <code>true</code> se estiver excluida, <code>false</code> caso contrário.
     */
    private boolean isExcluded(Class<? extends Persistable> clazz) {
        return this.exclusions.contains(clazz);
    }
}